﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Globalization;

namespace Toolkit.Web.Mvc.ExtensionMethods
{
    public static class ModelStateDictionaryExtensions
    {
        /// <summary>
        /// Retrieves a model state property as the data type specified.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the model state collection.</param>
        /// <returns>Type casted model state property.</returns>
        public static T Get<T>(this ModelStateDictionary instance, string key)
        {
            return Get<T>(instance, key, default(T), false);
        }

        /// <summary>
        /// (Overload) Retrieves a model state property as the data type specified. If no property is found, it returns the default value given.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the model state collection.</param>
        /// <param name="defaultValue">Default value to be returned if no valid model state property is found.</param>
        /// <returns>Type casted model state property.</returns>
        public static T Get<T>(this ModelStateDictionary instance, string key, T defaultValue)
        {
            return Get<T>(instance, key, defaultValue, true);
        }

        /// <summary>
        /// (Overload) Retrieves a model state property as the data type specified. If no property is found, it returns the default value given.
        /// </summary>
        /// <typeparam name="T">Property type to be returned.</typeparam>
        /// <param name="key">Associated key for the property in the model state collection.</param>
        /// <param name="defaultValue">Default value to be returned if no valid model state property is found.</param>
        /// <param name="useDefaultValue">Whether or not to use the default value if no value exists.</param>
        /// <returns>Type casted model state property.</returns>
        public static T Get<T>(this ModelStateDictionary instance, string key, T defaultValue, bool useDefaultValue)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            try
            {
                return (T)(object)instance[key].Value.AttemptedValue;
            }
            catch (NullReferenceException)
            {
                if (useDefaultValue) return defaultValue;
                throw new Exception(string.Format("ModelState property does not exist and no default was provided: {0}", key));
            }
            catch (InvalidCastException)
            {
                if (useDefaultValue) return defaultValue;
                throw new Exception(string.Format("ModelState property cannot be cast to the expected data type: {0}", key));
            }
        }

        /// <summary>
        /// Adds a value to the model state collection if a value does not already exists for the given key.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public static void Add(this ModelStateDictionary instance, string key, string value)
        {
            Add(instance, key, value, System.Threading.Thread.CurrentThread.CurrentCulture);
        }

        /// <summary>
        /// Adds a value to the model state collection if a value does not already exists for the given key.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="culture"></param>
        public static void Add(this ModelStateDictionary instance, string key, string value, CultureInfo culture)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            if (instance[key] != null)
            {
                throw new Exception(string.Format("A value with the given key already exists in the model state collection: {0}", key));
            }

            instance.Add(key, new ModelState()
            {
                Value = new ValueProviderResult(value, value, culture)
            });
        }

        /// <summary>
        /// Adds a value or updates an existing value in the model state collection.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public static void Set(this ModelStateDictionary instance, string key, string value)
        {
            Set(instance, key, value, System.Threading.Thread.CurrentThread.CurrentCulture);
        }

        /// <summary>
        /// Adds a value or updates an existing value in the model state collection.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="culture"></param>
        public static void Set(this ModelStateDictionary instance, string key, string value, CultureInfo culture)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            if (culture == null)
            {
                throw new ArgumentNullException();
            }

            instance.SetModelValue(key, new ValueProviderResult(value, value, culture));
        }

        /// <summary>
        /// Clears the value and any validation errors for a given key in the model state collection.
        /// </summary>
        /// <param name="key"></param>
        public static void Clear(this ModelStateDictionary instance, string key)
        {
            Clear(instance, key, System.Threading.Thread.CurrentThread.CurrentCulture);
        }

        /// <summary>
        /// Clears the value and any validation errors for a given key in the model state collection.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="culture"></param>
        public static void Clear(this ModelStateDictionary instance, string key, CultureInfo culture)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            if (culture == null)
            {
                throw new ArgumentNullException();
            }

            ModelState item = instance[key];
            if (item != null)
            {
                item.Value = new ValueProviderResult(null, null, culture);
                item.Errors.Clear();
            }
        }

        /// <summary>
        /// Clears out any errors associated with a given set of key prefixes. This can be used to ignore validation
        /// errors for sub-property objects.
        /// </summary>
        /// <param name="keyPrefixes">Parameter array of key prefixes (not including the final dot).</param>
        public static void ClearErrors(this ModelStateDictionary instance, params string[] keyPrefixes)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (keyPrefixes == null)
            {
                return;
            }

            List<string> keys = new List<string>();

            // Find keys in model state that start with any of the given prefixes.
            foreach (string prefix in keyPrefixes)
            {
                // Note: append dot to prefix to correctly match up the key prefixes.
                keys.AddRange(instance.Keys.Where(
                    k => k.StartsWith(string.Format("{0}.", prefix), StringComparison.OrdinalIgnoreCase)
                ));
            }

            // Clear validation errors for the keys found.
            foreach (string key in keys)
            {
                instance[key].Errors.Clear();
            }
        }

        /// <summary>
        /// Returns whether or not errors exist in the current model state for any of the given keys.
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="keys"></param>
        /// <returns></returns>
        public static bool ErrorsExistFor(this ModelStateDictionary instance, params string[] keys)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            foreach (string key in keys)
            {
                if (!instance.IsValidField(key))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Helper method to remove more then one model state at a time from a model state dictionary.
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="keys"></param>
        public static void Remove(this ModelStateDictionary instance, params string[] keys)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            foreach (string key in keys)
            {
                instance.Remove(key);
            }
        }

        /// <summary>
        /// Clears out any values associated with a given set of key prefixes.
        /// </summary>
        /// <param name="keyPrefixes">Parameter array of key prefixes (not including the final dot).</param>
        public static void ClearValues(this ModelStateDictionary instance, params string[] keyPrefixes)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (keyPrefixes == null)
            {
                return;
            }

            List<string> keys = new List<string>();

            // Find keys in model state that start with any of the given prefixes.
            foreach (string prefix in keyPrefixes)
            {
                // Note: append dot to prefix to correctly match up the key prefixes.
                keys.AddRange(instance.Keys.Where(
                    k => k.StartsWith(string.Format("{0}.", prefix), StringComparison.OrdinalIgnoreCase)
                ));
            }

            // Clear values for the keys found.
            foreach (string key in keys)
            {
                instance.Clear(key, System.Threading.Thread.CurrentThread.CurrentCulture);
            }
        }

        /// <summary>
        /// Merges the specified instance.
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="replaceExisting"></param>
        public static void Merge(this ModelStateDictionary instance, string key, ModelState value, bool replaceExisting)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException();
            }

            if (value == null)
            {
                throw new ArgumentNullException();
            }

            if (replaceExisting || !instance.ContainsKey(key))
            {
                instance[key] = value;
            }
        }

        /// <summary>
        /// Merges the specified instance.
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="from"></param>
        /// <param name="replaceExisting"></param>
        public static void Merge(this ModelStateDictionary instance, ModelStateDictionary from, bool replaceExisting)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }

            if (from == null)
            {
                throw new ArgumentNullException();
            }

            foreach (KeyValuePair<string, ModelState> pair in from)
            {
                instance.Merge(pair.Key, pair.Value, replaceExisting);
            }
        }
    }
}
